/*
 
(c) 2008 by Sławek Kruczek
slav.mir@gmail.com
 

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/


package application;

import analytics.util.BinaryFunction;
import java.beans.PropertyChangeEvent;
import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import com.sun.opengl.util.j2d.TextRenderer;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeListener;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import javax.swing.SwingWorker;



public class GLFunctionCanvas extends GLCanvas implements GLEventListener {

    private int range = 10;
    private int detail = 10;
    private BinaryFunction func = null;
    private float distance = 1;
    private int xyRotation = -45;
    private int zRotation = 0;
    private float horizontalOffset = 0;
    private float verticalOffset = 0;
    private boolean updating = true;
    private boolean paintingGrid = true;
    
    private Calculator calculator = null;
    
    private final int graphDisplayListId = 1;
    private final int gridDisplayListId = 2;
    private boolean functionChanged = true;
    private boolean updateSnapshot = false;
    
    private TextRenderer textRenderer;
    
    ArrayList<PropertyChangeListener> propertyListeners = new ArrayList<PropertyChangeListener>();
    
    private BufferedImage snapshot = null;
   
    
    
    public GLFunctionCanvas() {
        super();
        addGLEventListener(this);
    }
    
    public void setRange(int range) {
        if(range < 0) {
            throw new IllegalArgumentException();
        }
        
        this.range = range;
        functionChanged = true;
        doMath();
    }
    
    public void setDetail(int detail) {
        if(detail < 1) {
            throw new IllegalArgumentException();
        }
        
        this.detail = detail;
        functionChanged = true;
        doMath();
    }
    
    public void setFunction(BinaryFunction f) {
        func = f;
        functionChanged = true;
        doMath();
    }
    
    public BinaryFunction getFunction() {
        return func;
    }
    
    public void setDistance(float dist) {
        this.distance = dist;
        display();
    }
    
    public float getDistance() {
        return distance;
    }
    
    public void setXyRotation(int angle) {
        xyRotation = angle;
        display();
    }
    
    public int getXyRotation() {
        return xyRotation;
    }
    
    public void setZRotation(int angle) {
        zRotation = angle;
        display();
    }
    
    public int getZRotation() {
        return zRotation;
    }
    
    public void setHorizontalOffset(float offset) {
        horizontalOffset = offset;
        display();
    }
    
    public float getHorizontalOffset() {
        return horizontalOffset;
    }
    
    public void setVerticalOffset(float offset) {
        verticalOffset = offset;
        display();
    }
    
    public float getVerticalOffset() {
        return verticalOffset;
    }
    
    public boolean isUpdating() {
        return updating;
    }
    
    public void setUpdating(boolean value) {
        updating = value;
    }
    
    public boolean isPaintingGrid() {
        return paintingGrid;
    }
    
    public void setPaintingGrid(boolean value) {
        paintingGrid = value;
    }
    
    public void addProgressListener(PropertyChangeListener l) {
        propertyListeners.add(l);
    }
    
    public void removeProgressListener(PropertyChangeListener l) {
        propertyListeners.remove(l);
    }
    
    
    private void doMath() {
        // cancel the old calculator if it's still running
        if(calculator != null) {
            calculator.cancel(true);
        }
        
        if(func != null) {
            calculator = new Calculator(func, range, detail);

            // what to do when the work is finished
            calculator.addPropertyChangeListener(new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent evt) {
                    if(evt.getPropertyName().equals("state") &&  evt.getNewValue() == SwingWorker.StateValue.DONE) {
                        if(calculator != null && !calculator.isCancelled()) {
                            display();
                        }
                        else {
                            calculator = null;
                        }
                    }
                    else if(evt.getPropertyName().equals("progress")) {
                        for(PropertyChangeListener l: propertyListeners) {
                            l.propertyChange(evt);
                        }
                    }
                }
            });

            calculator.execute();     
        }
        else {
            // show an empty graph
            display();
        }
    }
    
    
    public BufferedImage getSnapshot() {
        updateSnapshot = true;
        display();
        updateSnapshot = false;
        
        return snapshot;
        
    }
    
    
    // help was found here http://www.felixgers.de/teaching/jogl/imagingProg.html
    private void updateSnapshot(GL gl) {
        int width = getWidth();
        int height = getHeight();

        
        ByteBuffer pixelData = ByteBuffer.allocateDirect(width * height * 4);
        
        gl.glReadBuffer(GL.GL_BACK);
        gl.glPixelStorei(GL.GL_PACK_ALIGNMENT, 1);
        gl.glReadPixels(0, 0, width, height, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pixelData);
        
        
        // converting to argb and flipping the image
        IntBuffer intPixelData = pixelData.asIntBuffer();
        int[] finalPixelData = new int[width * height];
        
        int srcRow = 0;
        int destRow = height - 1;
        
        while(destRow > 0) {
            int srcIndex = srcRow * width;
            int destIndex = destRow * width;
            
            for(int col = 0; col < width; col++) {
                finalPixelData[destIndex + col] = (intPixelData.get(srcIndex + col) >> 8) | 0xFF000000;
            }
            
            destRow--;
            srcRow++;
        }
        
        // for some reason there's a blank line at the top of the image. For the time being, I just crop the image
        // by 1 pixel
        snapshot = new BufferedImage(width, height - 1, BufferedImage.TYPE_INT_ARGB);
        snapshot.setRGB(0, 0, width, height - 1, finalPixelData, width, width);
    }
    
    
    // GLEventListener implementation
    
    public void display(GLAutoDrawable drawable) {
        if(updating) {
            GL gl = drawable.getGL();
            gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

            gl.glPushMatrix();

            gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, new float[]{1, 1, .5f, 0}, 0);

            gl.glTranslatef(horizontalOffset, verticalOffset, -distance);
            gl.glRotatef(-45, 0, 0, 1);
            gl.glRotatef(xyRotation, 1, 1, 0);
            gl.glRotatef(zRotation, 0, 0, 1);

            
            if (func != null) {
                if (functionChanged && calculator != null && calculator.isDone()) {
                    
                    try {
                        gl.glNewList(graphDisplayListId, GL.GL_COMPILE);
                        
                        double[][][] points = calculator.get();
                        
                        if(points != null) {
                            int length = 2 * range * detail;

                            // for each row k along the y axis
                            for (int k = 0; k < length; k++) {

                                gl.glBegin(GL.GL_TRIANGLE_STRIP);

                                for (int j = 0; j <= length; j++) {

                                    double x = (j - length / 2.0) / detail;
                                    double y = (k + 1 - length / 2.0) / detail;
                                    double[] v = points[j][k + 1];

                                    gl.glColor3d(.3, (Math.PI / 2 + Math.atan(.3 * v[0])) / Math.PI, .8);
                                    gl.glNormal3d(-v[1], -v[2], 1);
                                    gl.glVertex3d(x, y, v[0]);

                                    y = (k - length / 2.0) / detail;
                                    v = points[j][k];
                                    gl.glColor3d(.3, (Math.PI / 2 + Math.atan(.3 * v[0])) / Math.PI, .8);
                                    gl.glNormal3d(-v[1], -v[2], 1);
                                    gl.glVertex3d(x, y, v[0]);
                                }

                                gl.glEnd();
                            }
                        }

                        gl.glEndList();
                    }
                    catch(Exception ex) {
                        // there's not much we can do, is there?
                    }
                    
                    if(!paintingGrid) {
                        functionChanged = false;
                        calculator = null;
                    }
                }

                gl.glCallList(graphDisplayListId);

            }

            
            if(paintingGrid) {
                // Drawing the translucent grid - makes sense to disable lighting
                gl.glDisable(GL.GL_LIGHTING);
                gl.glEnable(GL.GL_BLEND);
                gl.glEnable(GL.GL_LINE_SMOOTH);
                gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
                gl.glDepthMask(false);

                if (functionChanged &&  calculator != null && calculator.isDone() && !calculator.isCancelled()) {
                    gl.glNewList(gridDisplayListId, GL.GL_COMPILE);
                    
                    gl.glBegin(GL.GL_LINES);
                    
                    int length = 2 * range * detail;
                    for (int i = 0; i <= length; i++) {
                        float v = (i - length / 2.0f) / detail;


                        if (i % detail == 0) {
                            gl.glColor4f(1, 1, 1, .1f);
                        }
                        else {
                            gl.glColor4f(1, 1, 1, .06f);
                        }

                        gl.glVertex3f(v, -range, 0);
                        gl.glVertex3f(v, range, 0);
                        gl.glVertex3f(-range, v, 0);
                        gl.glVertex3f(range, v, 0);
                    }

                    
                    float corners[][] = {
                        {-range, -range, -range},
                        {-range, -range, range},
                        {-range, range, -range},
                        {range, -range, -range},
                        {range, range, -range},
                        {range, -range, range},
                        {-range, range, range},
                        {range, range, range},
                         
                         };
                    
                    for(float[] corner: corners) {
                        gl.glVertex3fv(corner, 0);
                        gl.glVertex3f(corner[0] - Math.signum(corner[0]), corner[1], corner[2]);

                        gl.glVertex3fv(corner, 0);
                        gl.glVertex3f(corner[0], corner[1] - Math.signum(corner[1]), corner[2]);

                        gl.glVertex3fv(corner, 0);
                        gl.glVertex3f(corner[0], corner[1], corner[2] - Math.signum(corner[2]));

                    }

                    gl.glColor4f(1, 1, 1, .25f);
                    gl.glVertex3f(0, 0, 0);
                    gl.glVertex3f(range + 1.5f, 0, 0);
                    gl.glVertex3f(0, 0, 0);
                    gl.glVertex3f(0, range + .5f, 0);
                    gl.glVertex3f(0, 0, 0);
                    gl.glVertex3f(0, 0, range + .5f);
                    
                    gl.glEnd();
                    gl.glEndList();
                    
                    functionChanged = false;
                    calculator = null;
                }

                gl.glCallList(gridDisplayListId);
 
                
                textRenderer.beginRendering(drawable.getWidth(), drawable.getHeight());
                textRenderer.setColor(1, 1, 1, 0.3f);
                textRenderer.draw("threeDee 8.05", 10, 20);
                textRenderer.draw("© Sławek Kruczek", 10, 10);
                textRenderer.endRendering();

                gl.glDisable(GL.GL_BLEND);
                gl.glEnable(GL.GL_LIGHTING);
                gl.glDepthMask(true);
            }
            
            gl.glPopMatrix();
            gl.glFlush();
            
            if(updateSnapshot) {
                updateSnapshot(gl);
            }
        }
    }
    
    public void init(GLAutoDrawable drawable) {
        GL gl = drawable.getGL();
        
        // turning on vsync
        gl.setSwapInterval(1);
        
        gl.glClearColor(.0f, .0f, 0.f, .0f);
        
        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glEnable(GL.GL_LIGHTING);
        gl.glEnable(GL.GL_NORMALIZE);
        gl.glEnable(GL.GL_COLOR_MATERIAL);
        gl.glColorMaterial(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT_AND_DIFFUSE);
        
        gl.glShadeModel(GL.GL_SMOOTH);
        gl.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_NICEST);
        
        // setting up the light
        float[] ambient_light = {.2f, .2f, .2f, 1};
        gl.glLightModelfv(GL.GL_LIGHT_MODEL_AMBIENT, ambient_light, 0);
        
        float[] diffuse_light = {1, 1, 1, 1 };
        float[] specular_light = { 1, 1, 1, 1.0f };
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, diffuse_light, 0);
        gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, specular_light, 0);
        
        gl.glEnable(GL.GL_LIGHT0);
        
        textRenderer = new TextRenderer(new Font("SansSerif", Font.BOLD, 9));
    }

    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        GL gl = drawable.getGL();
        GLU glu = new GLU();
        
        gl.glViewport(0, 0, width, height);
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();
        
        glu.gluPerspective(70, (float)width / height, 1, 47);
        
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glTranslatef(0, 0, -2.0f);  
    }

    public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) {
        // it's never used...
    }
}
